import xapian
import datetime
from mbxap.tokens import Tokenizer
from mbxap.numrange import lessthan, greaterthan, flatten_range

__all__ = ['AND', 'OR', 'XOR', 'NEAR', 'PHRASE', 'FILTER', 'AND_MAYBE', 'AND_NOT', 'ELITE_SET', 'Tokenizer']

###
### Tokenizer for general query parsing
###


ops = ['OP_AND', 'OP_AND_NOT', 'OP_FILTER', 'OP_OR', 'OP_XOR', 'OP_AND_MAYBE', 'OP_ELITE_SET', 'OP_NEAR', 'OP_PHRASE']
XAP_MAP = {}
for op in ops:
    XAP_MAP[getattr(xapian.Query, op)] = op


###
### Query object for query construction
###

class _Query(object):
    '''
    A query can be built with a list of any combination of the following
    objects:
        * strings
        * query objects
        * Field objects
    '''
    def __init__(self, terms):
        self._terms = terms

    def as_xapian(self, stemmap=None):
        '''
        Recurse down to any terms that are instances of Query
        and generate a proper Xapian query object.

        Passing in a stemmer map will cause the query to run through an
        automatic expansion based on historical stemmed form expansion data.
        '''
        tmp_terms = self._terms[:]

        for i, t in enumerate(tmp_terms):
            # Recurse down any query objects
            if isinstance(t, _Query):
                tmp_terms[i] = t.as_xapian(stemmap)
            elif isinstance(t, str):
                pass
        if len(tmp_terms) == 1 and  XAP_MAP[self._operator] == 'OP_AND':
            if stemmap:
                t = tmp_terms[0]
                if ":" in t:
                    t = ':'.join(t.split(":")[1:])
                terms = list(stemmap.expand(t))
                if len(terms) ==  1:
                    return xapian.Query(terms[0])
                else:
                    return xapian.Query(xapian.Query.OP_OR, terms)

            return xapian.Query(tmp_terms[0])
        else:
            return xapian.Query(self._operator, tmp_terms)

    def as_json(self):
        # Recurse down to any terms that are instances of Query
        tmp_terms = self._terms[:]

        for i, t in enumerate(tmp_terms):
            # Recurse down any query objects
            if isinstance(t, _Query):
                tmp_terms[i] = t.as_json()
            elif isinstance(t, str):
                pass

        # Don't render single term AND operators
        if len(tmp_terms) == 1 and XAP_MAP[self._operator] == 'OP_AND':
            return tmp_terms[0]
        else:
            return {XAP_MAP[self._operator]: tmp_terms}

    def get_description(self):
        return self.as_xapian().get_description()

def _op_handler(*operands, **kw_operands):
    result = []
    for i, op in enumerate(operands):
        # Note that we do not accept unicode strings here as we
        # assume that all internal data is UTF8 clean.
        if isinstance(op, str):
            # tokenize the inputs if necessary
            # and convert to AND objects
            for token in Tokenizer(op):
                result.append(_AND([token]))
        elif isinstance(op, _Query):
            result.append(op)
        else:
            raise RuntimeError, ("Unexpected datatype in query", op)

    # sort the items so that we always fields in alphabetic order
    sorted_items = kw_operands.items()
    sorted_items.sort(lambda (x1,x2), (y1,y2): cmp("%s.%s" % (x1,x2), "%s.%s" % (y1,y2)))

    for i, (field_name, op) in enumerate(sorted_items):
        # Note that we do not accept unicode strings here as we
        # assume that all internal data is UTF8 clean.
        # tokenize the inputs if necessary
        # and convert to AND objects
        for token in Tokenizer(op):
            result.append(_AND(["%s:%s" % (field_name, token)]))

    return result



###
### supported operators
###

class _AND(_Query):
    _operator = xapian.Query.OP_AND
class _OR(_Query):
    _operator = xapian.Query.OP_OR
class _AND_MAYBE(_Query):
    _operator = xapian.Query.OP_AND_MAYBE
class _AND_NOT(_Query):
    _operator = xapian.Query.OP_AND_NOT
class _ELITE_SET(_Query):
    _operator = xapian.Query.OP_ELITE_SET
class _FILTER(_Query):
    _operator = xapian.Query.OP_FILTER
class _NEAR(_Query):
    _operator = xapian.Query.OP_NEAR
class _PHRASE(_Query):
    _operator = xapian.Query.OP_PHRASE
class _XOR(_Query):
    _operator = xapian.Query.OP_XOR


def NUM_RANGE(token_name, start, end):
    '''
    Create a range query for an integer.  Assume standard precision as defined
    in numrange.py

    At least the start or end must be non-null. A null on one end indicates an
    approximated open ended query.  It's not really open ended as the numeric
    search operates only on a fixed range of integers.
    '''
    assert start or end
    if start is None:
        # less than query
        query = OR(*flatten_range(lessthan(token_name, end)))
    elif end is None:
        query = OR(*flatten_range(greaterthan(token_name, start)))
    else:
        lt_terms = set(flatten_range(lessthan(token_name, end)))
        gt_terms = set(flatten_range(greaterthan(token_name, start)))
        term_set = lt_terms & gt_terms
        query = OR(*list(term_set))

    return query

    


def AND(*operands, **kw_operands):
    return _AND(_op_handler(*operands, **kw_operands))
def OR(*operands, **kw_operands):
    return _OR(_op_handler(*operands, **kw_operands))
def AND_MAYBE(*operands, **kw_operands):
    return _AND_MAYBE(_op_handler(*operands, **kw_operands))
def AND_NOT(*operands, **kw_operands):
    return _AND_NOT(_op_handler(*operands, **kw_operands))
def ELITE_SET(*operands, **kw_operands):
    return _ELITE_SET(_op_handler(*operands, **kw_operands))
def FILTER(*operands, **kw_operands):
    return _FILTER(_op_handler(*operands, **kw_operands))
def NEAR(*operands, **kw_operands):
    return _NEAR(_op_handler(*operands, **kw_operands))
def PHRASE(*operands, **kw_operands):
    # Need to expand the query here
    return _PHRASE(_op_handler(*operands, **kw_operands))

def XOR(*operands, **kw_operands):
    return _XOR(_op_handler(*operands, **kw_operands))


